#include <EXTERN.h>
#include <XSUB.h>
#include <perl.h>

EXTERN_C void xs_init (pTHX);

EXTERN_C void boot_DynaLoader (pTHX_ CV* cv);

#include "milter.h"

static XS(XS_milter_getpriv)
{
    struct milter_ctx *ctx = NULL;

    dXSARGS;

    (void)cv;

    if (items != 1)
        croak("Usage: getpriv(ctx)");

    if (SvROK(ST(0)))
        ctx = MILTER_CTX(ST(0));

    if (!ctx)
        croak("ctx is not a reference");

    if (!ctx->priv)
        XSRETURN_UNDEF;

    PUSHMARK(SP);
    XPUSHs(sv_2mortal(SvREFCNT_inc(ctx->priv)));
    PUTBACK;
}

static XS(XS_milter_getsymval)
{
    struct milter_ctx *ctx = NULL;
    SV **svp;
    char *key;
    STRLEN len;

    dXSARGS;

    (void)cv;

    if (items != 2)
        croak("Usage: getsymval(ctx, key)");

    if (SvROK(ST(0)))
        ctx = MILTER_CTX(ST(0));

    if (!ctx)
        croak("ctx is not a reference");

    key = SvPV(ST(1), len);

    if (!(svp = hv_fetch(ctx->macro, key, len, FALSE)))
        XSRETURN_UNDEF;

    PUSHMARK(SP);
    XPUSHs(sv_2mortal(SvREFCNT_inc(*svp)));
    PUTBACK;
}

static XS(XS_milter_setpriv)
{
    struct milter_ctx *ctx = NULL;

    dXSARGS;

    (void)cv;

    if (items != 2)
        croak("Usage: setpriv(ctx, value)");

    if (SvROK(ST(0)))
        ctx = MILTER_CTX(ST(0));

    if (!ctx)
        croak("ctx is not a reference");

    if (ctx->priv)
        SvREFCNT_dec(ctx->priv);

    ctx->priv = SvREFCNT_inc(ST(1));

    XSRETURN_YES;
}

// RCODE, XCODE, MESSAGE
static XS(XS_milter_setreply)
{
    struct milter_ctx *ctx = NULL;
    char *buf;
    STRLEN len;

    dXSARGS;

    (void)cv;

    if (items != 2 && items != 4)
        croak("Usage: setreply(ctx, [rcode, xcode,] reason)");

    if (SvROK(ST(0)))
        ctx = MILTER_CTX(ST(0));

    if (!ctx)
        croak("ctx is not a reference");

    if (items == 2)
    {
        buf = SvPV(ST(1), len);
    }
    else
    {
        buf = form("%3d %s %s", SvIV(ST(1)), SvPV_nolen(ST(2)), SvPV_nolen(ST(3)));
        len = strlen(buf);
    }

    /* XXX: fix up verification */
    if (!(buf[0] == '4' || buf[0] == '5') || !(isDIGIT(buf[1]) && isDIGIT(buf[2]) && buf[3] == ' ') || !(buf[0] == buf[4]))
        croak("malformed reply");

    // XXX: null-terminated? might need to grow/append ..
    assert(buf[len] == 0);

    ctx->reply = strdup(buf);

    XSRETURN_IV(*buf == '4' ? SMFIS_TEMPFAIL : SMFIS_REJECT);
}

// <NOARGS>
static XS(XS_milter_shutdown)
{
    struct milter_ctx *ctx = NULL;

    dXSARGS;

    (void)cv;

    if (items != 1)
        croak("Usage: shutdown(ctx)");

    if (SvROK(ST(0)))
        ctx = MILTER_CTX(ST(0));

    if (!ctx)
        croak("ctx is not a reference");

#define SHUTDOWN_REPLYCODE "421 4.7.0 Closing communications channel"
    assert(SHUTDOWN_REPLYCODE[sizeof(SHUTDOWN_REPLYCODE) - 1] == 0);

    ctx->reply = strdup(SHUTDOWN_REPLYCODE);

    XSRETURN_IV(SMFIS_TEMPFAIL);
}

// HEADER, VALUE
// Limit: eom
static XS(XS_milter_addheader)
{
    struct milter_ctx *ctx = NULL;
    char buf[2048];
    char *header, *value;
    STRLEN hlen, vlen;

    dXSARGS;

    (void)cv;

    if (items != 3)
        croak("Usage: addheader(ctx, header, value)");

    if (SvROK(ST(0)))
        ctx = MILTER_CTX(ST(0));

    if (!ctx)
        croak("ctx is not a reference");

    if (!(ctx->actions & SMFIF_ADDHDRS))
        croak("addheader action not available");

    if (ctx->code != SMFIC_BODYEOB)
        croak("addheader action only available in eom");

    header = SvPV(ST(1), hlen);
    value = SvPV(ST(2), vlen);

    // XXX: null-terminated? might need to grow/append ..
    assert(header[hlen] == 0);
    assert(value[vlen] == 0);
    assert(hlen + 1 + vlen + 1 < sizeof(buf));

    memcpy(&buf[0], header, hlen + 1);
    memcpy(&buf[hlen+1], value, vlen + 1);

    if (write_packet(ctx->sock, SMFIR_ADDHEADER, buf, hlen + 1 + vlen + 1) < 0)
        XSRETURN_NO;

    XSRETURN_YES;
}

// ADDRESS
// Limit: eom
static XS(XS_milter_addrcpt)
{
    struct milter_ctx *ctx = NULL;
    char *address;
    STRLEN len;

    dXSARGS;

    (void)cv;

    if (items != 2)
        croak("Usage: addrcpt(ctx, address)");

    if (SvROK(ST(0)))
        ctx = MILTER_CTX(ST(0));

    if (!ctx)
        croak("ctx is not a reference");

    if (!(ctx->actions & SMFIF_ADDRCPT))
        croak("addrcpt action not available");

    if (ctx->code != SMFIC_BODYEOB)
        croak("addrcpt action only available in eom");

    address = SvPV(ST(1), len);

    // XXX: null-terminated? might need to grow/append ..
    assert(address[len] == 0);

    if (write_packet(ctx->sock, SMFIR_ADDRCPT, address, len + 1) < 0)
        XSRETURN_NO;

    XSRETURN_YES;
}

// HEADER, INDEX, VALUE
// Limit: eom
static XS(XS_milter_chgheader)
{
    struct milter_ctx *ctx = NULL;
    char buf[2048];
    int index;
    char *header, *value;
    STRLEN hlen, vlen;

    dXSARGS;

    (void)cv;

    if (items != 4)
        croak("Usage: addheader(ctx, header, index, value)");

    if (SvROK(ST(0)))
        ctx = MILTER_CTX(ST(0));

    if (!ctx)
        croak("ctx is not a reference");

    if (!(ctx->actions & SMFIF_CHGHDRS))
        croak("chgheader action not available");

    if (ctx->code != SMFIC_BODYEOB)
        croak("chgheader action only available in eom");

    header = SvPV(ST(1), hlen);
    index = SvIV(ST(2));
    value = SvPV(ST(3), vlen);

    // XXX: null-terminated? might need to grow/append ..
    assert(header[hlen] == 0);
    assert(value[vlen] == 0);
    assert(4 + hlen + 1 + vlen + 1 < sizeof(buf));

    *(uint32_t *)&buf[0] = htonl(index);
    memcpy(&buf[4], header, hlen + 1);
    memcpy(&buf[4+hlen+1], value, vlen + 1);

    if (write_packet(ctx->sock, SMFIR_CHGHEADER, buf, 4 + hlen + 1 + vlen + 1) < 0)
        XSRETURN_NO;

    XSRETURN_YES;
}

// ADDRESS
// Limit: eom
static XS(XS_milter_delrcpt)
{
    struct milter_ctx *ctx = NULL;
    char *address;
    STRLEN len;

    dXSARGS;

    (void)cv;

    if (items != 2)
        croak("Usage: delrcpt(ctx, address)");

    if (SvROK(ST(0)))
        ctx = MILTER_CTX(ST(0));

    if (!ctx)
        croak("ctx is not a reference");

    if (!(ctx->actions & SMFIF_DELRCPT))
        croak("delrcpt action not available");

    if (ctx->code != SMFIC_BODYEOB)
        croak("delrcpt action only available in eom");

    address = SvPV(ST(1), len);

    // XXX: null-terminated? might need to grow/append ..
    assert(address[len] == 0);

    if (write_packet(ctx->sock, SMFIR_DELRCPT, address, len + 1) < 0)
        XSRETURN_NO;

    XSRETURN_YES;
}

// <NOARGS>
static XS(XS_milter_progress)
{
    struct milter_ctx *ctx = NULL;

    dXSARGS;

    (void)cv;

    if (items != 1)
        croak("Usage: progress(ctx)");

    if (SvROK(ST(0)))
        ctx = MILTER_CTX(ST(0));

    if (!ctx)
        croak("ctx is not a reference");

    if (write_packet(ctx->sock, SMFIR_PROGRESS, NULL, 0) < 0)
        XSRETURN_NO;

    XSRETURN_YES;
}

// REASON
// Limit: eom
static XS(XS_milter_quarantine)
{
    struct milter_ctx *ctx = NULL;
    char *reason;
    STRLEN len;

    dXSARGS;

    (void)cv;

    if (items != 2)
        croak("Usage: quarantine(ctx, reason)");

    if (SvROK(ST(0)))
        ctx = MILTER_CTX(ST(0));

    if (!ctx)
        croak("ctx is not a reference");

    if (!(ctx->actions & SMFIF_QUARANTINE))
        croak("quarantine action not available");

    if (ctx->code != SMFIC_BODYEOB)
        croak("quarantine action only available in eom");

    reason = SvPV(ST(1), len);

    // XXX: null-terminated? might need to grow/append ..
    assert(reason[len] == 0);

    if (write_packet(ctx->sock, SMFIR_QUARANTINE, reason, len + 1) < 0)
        XSRETURN_NO;

    XSRETURN_YES;
}

// CHUNK
// Limit: eom
static XS(XS_milter_replacebody)
{
    struct milter_ctx *ctx = NULL;
    char *body;
    STRLEN len;

    dXSARGS;

    (void)cv;

    if (items != 2)
        croak("Usage: replacebody(ctx, body)");

    if (SvROK(ST(0)))
        ctx = MILTER_CTX(ST(0));

    if (!ctx)
        croak("ctx is not a reference");

    if (!(ctx->actions & SMFIF_CHGBODY))
        croak("replacebody action not available");

    if (ctx->code != SMFIC_BODYEOB)
        croak("replacebody action only available in eom");

    body = SvPV(ST(1), len);

    if (write_packet(ctx->sock, SMFIR_REPLBODY, body, len) < 0)
        XSRETURN_NO;

    XSRETURN_YES;
}

// SENDER
// Limit: eom
static XS(XS_milter_setsender)
{
    struct milter_ctx *ctx = NULL;
    char *address;
    STRLEN len;

    dXSARGS;

    (void)cv;

    if (items != 2)
        croak("Usage: setsender(ctx, address)");

    if (SvROK(ST(0)))
        ctx = MILTER_CTX(ST(0));

    if (!ctx)
        croak("ctx is not a reference");

    if (!(ctx->actions & SMFIF_SETSENDER))
        croak("setsender action not available");

    if (ctx->code != SMFIC_BODYEOB)
        croak("setsender action only available in eom");

    address = SvPV(ST(1), len);

    // XXX: null-terminated? might need to grow/append ..
    assert(address[len] == 0);

    if (write_packet(ctx->sock, SMFIR_SETSENDER, address, len + 1) < 0)
        XSRETURN_NO;

    XSRETURN_YES;
}

EXTERN_C void xs_init(pTHX)
{
    char *file = __FILE__;
    dXSUB_SYS;

    /* DynaLoader is a special case */
    newXS("DynaLoader::boot_DynaLoader", boot_DynaLoader, file);

    newXS("Milter::getpriv", XS_milter_getpriv, file);
    newXS("Milter::getsymval", XS_milter_getsymval, file);
    newXS("Milter::setpriv", XS_milter_setpriv, file);
    newXS("Milter::setreply", XS_milter_setreply, file);
    newXS("Milter::shutdown", XS_milter_shutdown, file);
    newXS("Milter::addheader", XS_milter_addheader, file);
    newXS("Milter::addrcpt", XS_milter_addrcpt, file);
    newXS("Milter::chgheader", XS_milter_chgheader, file);
    newXS("Milter::delrcpt", XS_milter_delrcpt, file);
    newXS("Milter::progress", XS_milter_progress, file);
    newXS("Milter::quarantine", XS_milter_quarantine, file);
    newXS("Milter::replacebody", XS_milter_replacebody, file);
    newXS("Milter::setsender", XS_milter_setsender, file);
}
